import * as THREE from "three";
import { Scene } from "three";
import { DragControls } from "three/examples/jsm/controls/DragControls";
import Stats from "three/examples/jsm/libs/stats.module";
import { onMounted, reactive, Ref, ref, render, watchEffect } from "vue";
import balls_texture_path from "../assets/ball_img_path";
import table_texture_path from "./assets/table.jpg";

export enum UiStatus {
  wait,
  grab,
  shuttle,
}
export const uiStatus = ref(UiStatus.wait);

const ball_radius = 0.285;
const hole_radius = 0.427;
const table_width = 25.4;
const table_height = 12.7;
const ballTableClashExtent = {
  top: table_height / 2 - ball_radius,
  bottom: -table_height / 2 + ball_radius,
  right: table_width / 2 - ball_radius,
  left: -table_width / 2 + ball_radius,
};
class Myball extends THREE.Mesh {
  velocity;
  initPosition;
  draged;
  isCueBall = false;
  constructor(
    geometry: THREE.BufferGeometry,
    material: THREE.Material,
    attr: {
      velocity: THREE.Vector2;
      initPosition: THREE.Vector3;
    }
  ) {
    super(geometry, material);
    this.layers.set(0);
    this.velocity = attr.velocity;
    this.initPosition = attr.initPosition;
    this.position.set(
      attr.initPosition.x,
      attr.initPosition.y,
      attr.initPosition.z
    );
    this.draged = false;
  }
  reset() {
    this.layers.set(0);
    this.position.x = 0;
    this.position.y = 0;
    this.velocity.x = 0;
    this.velocity.y = 0;
  }
  dispose() {
    this.layers.set(1);
    this.position.x = 0;
    this.position.y = 0;
    this.velocity.x = 0;
    this.velocity.y = 0;
  }
}

/**
 * 摩擦减速
 * @param v
 * @param t
 */
const vFriction = (() => {
  /** 摩擦系数 */
  const friction = 0.27 * 1e-3;
  return (v: THREE.Vec2) => {
    const abs = Math.abs(v.x) + Math.abs(v.y);
    if (abs < 2 * friction) {
      v.x = 0;
      v.y = 0;
      return;
    }
    const scale = Math.sqrt(1 - (2 * friction) / abs);
    v.x *= scale;
    v.y *= scale;
  };
})();
class Billiards {
  _uiStatus;
  scene = new THREE.Scene();
  camera = new THREE.PerspectiveCamera(
    60,
    window.innerWidth / window.innerHeight,
    1,
    0
  );
  // camera = new THREE.OrthographicCamera(-table_width/2,table_width/2,table_height/2,-table_height/2,0.1,0)
  renderer = new THREE.WebGLRenderer({
    antialias: true,
  });
  stats = (function createStats() {
    const stats = Stats();
    stats.dom.style.position = "absolute";
    stats.dom.style.left = "0";
    stats.dom.style.top = "0";
    return stats;
  })();
  allBallsStatic = true;
  balls;
  animate;
  controls;
  constructor(uiStatus: Ref<UiStatus>) {
    // =====Env Init=======
    this._uiStatus = uiStatus;
    this.camera.lookAt(new THREE.Vector3(0, 0, 0));
    this.renderer.setSize(window.innerWidth, window.innerHeight);
    this.onWindowResize();
    this._initEnv();
    // =====Balls&Holls========
    const holes = this._createHoles();
    holes.forEach((e) => this.scene.add(e));
    const balls = this._createBalls();
    balls.forEach((ball) => {
      this.scene.add(ball);
    });
    this.balls = balls;
    // ========Animation&Motion
    const moveAndClash = (() => {
      /** 精度 */
      const precision = 1e-4;
      /** 球桌碰撞速度损失 */
      const tableClashBallVelocityRatio = 0.77;
      const motionLayer = new THREE.Layers();
      motionLayer.set(0);
      return () => {
        // 当前
        const liveBalls = balls.filter(
          (ball) => ball.layers.test(motionLayer) && !ball.draged
        );
        //静止检测
        let ballsStaticFlag = true;
        for (let ball of liveBalls) {
          if (
            Math.abs(ball.velocity.x) > precision ||
            Math.abs(ball.velocity.y) > precision
          ) {
            ballsStaticFlag = false;
            break;
          } else {
          }
        }
        //更新UI状态
        let shouldStatus;
        if (this._uiStatus.value === UiStatus.grab) {
          shouldStatus = UiStatus.grab;
        } else {
          shouldStatus = ballsStaticFlag ? UiStatus.shuttle : UiStatus.wait;
        }
        if (this._uiStatus.value !== shouldStatus)
          this._uiStatus.value = shouldStatus;

        this.allBallsStatic = ballsStaticFlag;

        // 运动
        liveBalls.forEach((ball, index, pBalls) => {
          let distance = 0;
          if (!ball.draged) {
            // 速度损耗
            // delta_v = sqrt( v^2 - alpha*v*t*2)
            vFriction(ball.velocity);
            let deltaX = ball.velocity.x;
            let deltaY = ball.velocity.y;
            // let displace = new THREE.Vector3(deltaX, deltaY, 0);
            distance = Math.sqrt(deltaX ** 2 + deltaY ** 2);
            // 位置变化
            ball.position.x += deltaX;
            ball.position.y += deltaY;

            //运动旋转
            if (distance) {
              const directOnObject = new THREE.Vector3(
                -deltaY,
                deltaX,
                0
              ).multiplyScalar(1 / distance);
              distance &&
                ball.rotateOnWorldAxis(directOnObject, distance / ball_radius);
            }
          }
          // 进球检测
          const getInHole = holes.reduce((p, c) => {
            return (
              p ||
              ball.position.clone().distanceToSquared(c.position) <= hole_radius
            );
          }, false);
          if (getInHole) {
              ball.dispose();
            return;
          }
          // 球体碰撞
          if (index < pBalls.length - 1) {
            for (let j = index + 1; j < pBalls.length; j++) {
              let ballA = pBalls[index];
              let ballB = pBalls[j];
              let vecAB = ballB.position.clone().sub(ballA.position);
              if (vecAB.x === 0 && vecAB.y === 0) {
                vecAB.x = 1;
              }
              let dis = Math.sqrt(vecAB.x ** 2 + vecAB.y ** 2);
              let patchDis = (2 * ball_radius - dis) / 2;

              if (patchDis >= 0) {
                let directionAB = new THREE.Vector2(
                  vecAB.x,
                  vecAB.y
                ).multiplyScalar(1 / dis);
                let patchVecB = vecAB.clone().multiplyScalar(patchDis / dis);
                let patchVecA = patchVecB.clone().multiplyScalar(-1);
                ballA.position.add(patchVecA);
                ballB.position.add(patchVecB);
                let vecVeloVa = directionAB.dot(ballA.velocity);
                let vecVeloVb = directionAB.dot(ballB.velocity);
                ballA.velocity.add(
                  directionAB.clone().multiplyScalar(+vecVeloVb - vecVeloVa)
                );
                ballB.velocity.add(
                  directionAB.clone().multiplyScalar(-vecVeloVb + vecVeloVa)
                );
              }
            }
          }
          // 球桌碰撞
          if (ball.position.x >= ballTableClashExtent.right) {
            ball.position.x = ballTableClashExtent.right;
            ball.velocity.x = -ball.velocity.x * tableClashBallVelocityRatio;
          } else if (ball.position.x <= ballTableClashExtent.left) {
            ball.position.x = ballTableClashExtent.left;
            ball.velocity.x = -ball.velocity.x * 0.77;
          }

          if (ball.position.y >= ballTableClashExtent.top) {
            ball.position.y = ballTableClashExtent.top;
            ball.velocity.y = -ball.velocity.y * 0.77;
          } else if (ball.position.y <= ballTableClashExtent.bottom) {
            ball.position.y = ballTableClashExtent.bottom;
            ball.velocity.y = -ball.velocity.y * 0.77;
          }
        });
      };
    })();
    this.animate = () => {
      // remote
      this.stats.update();
      if (uiStatus.value === UiStatus.shuttle && balls[0].layers.mask !== 1) balls[0].reset();
      moveAndClash();
      requestAnimationFrame(this.animate);
      this.renderer.render(this.scene, this.camera);
    };
    // =====Controls=====
    const controls = new DragControls(
      this.balls,
      this.camera,
      this.renderer.domElement
    );
    controls.addEventListener("dragstart", (e) => {
      const object = e.object as Myball;
      object.position.z = ball_radius * 3;
      // object.velocity = dragedBallReleaseVelocity;
      const material = object.material;
      // Highlight
      if (material instanceof THREE.MeshPhongMaterial) {
        material.emissive.set(0x777777);
        material.opacity = 0.5;
      }
      // draged flag
      object.draged = true;
    });
    controls.addEventListener("dragend", (e) => {
      const object = e.object as Myball;
      // object.velocity = dragedBallReleaseVelocity;
      const material = object.material;
      // Highlight
      if (material instanceof THREE.MeshPhongMaterial) {
        material.emissive.set(0x000000);
        material.opacity = 1;
      }
      // draged flag
      object.draged = false;
      object.position.z = ball_radius;
      // dragedBallReleaseVelocity = new THREE.Vector2(0, 0);
    });
    controls.deactivate();
    this.controls = controls;
  }

  screenToWorld(x: number, y: number) {
    const pointerPos = new THREE.Vector3(
      (x / this.renderer.domElement.width) * 2 - 1,
      -(y / this.renderer.domElement.height) * 2 + 1,
      0.5
    );
    pointerPos.unproject(this.camera);
    pointerPos.multiplyScalar(
      (this.camera.position.z - ball_radius) /
        (this.camera.position.z - pointerPos.z)
    );
    return pointerPos;
  }
  private _createHoles() {
    const holeList = [];
    const material = new THREE.MeshBasicMaterial({
      color: 0x222222,
    });
    let geometry = new THREE.CircleGeometry(ball_radius * 1.5, 32);
    for (let i = 0; i < 3; i++) {
      let hole1 = new THREE.Mesh(geometry, material);
      hole1.position.x = (table_width / 2) * (i - 1);
      hole1.position.y = table_height / 2;
      let hole2 = hole1.clone();
      hole2.position.y = -hole1.position.y;
      holeList.push(hole1, hole2);
    }
    return holeList;
  }
  onWindowResize() {
    const aspectRatio = window.innerWidth / window.innerHeight;
    this.camera.aspect = aspectRatio;
    this.camera.position.z = Math.max(
      ((table_width / 2) * Math.tan(Math.PI / 3)) / aspectRatio,
      (table_height / 2) * Math.tan(Math.PI / 3)
    );
    this.camera.far = this.camera.position.z;
    this.camera.updateProjectionMatrix();
    this.renderer.setSize(window.innerWidth, window.innerHeight);
  }
  private _initEnv() {
    const geometry = new THREE.PlaneGeometry(table_width, table_height);
    // const tableTexture = new THREE.TextureLoader().load(table_texture_path);
    const material = new THREE.MeshLambertMaterial({
      color: 0x114411,
      // map: tableTexture
    });
    const plane = new THREE.Mesh(geometry, material);
    plane.position.set(0, 0, 0);
    this.scene.add(plane);

    const light = new THREE.DirectionalLight(0xffffff, 0.9);
    light.position.set(0, 0, 4000);
    this.scene.add(light);
    const amLight = new THREE.AmbientLight(0xffffff, 0.7);
    this.scene.add(amLight);
  }
  private _createBalls() {
    const ball_geo = new THREE.SphereGeometry(ball_radius);
    const balls = balls_texture_path.map((u, i, a) => {
      const texture_ball = new THREE.TextureLoader().load(u);
      var ball_material = new THREE.MeshPhongMaterial({
        color: 0x999999,
        shininess: 20,
        map: texture_ball, //设置纹理贴图
      });
      // const ball = new THREE.Mesh(ball_geo, ball_material);
      return new Myball(ball_geo, ball_material, {
        velocity: new THREE.Vector2(ball_radius * 0, ball_radius * 0),
        initPosition: new THREE.Vector3(
          (a.length - 2 * i) * ball_radius,
          0,
          ball_radius
        ),
      });
    });
    console.dir({
      balls,
    });
    // 设置主球
    balls[0].isCueBall = true;
    return balls;
  }
  mounted(container: HTMLElement) {
    container.append(this.stats.dom);
    container.append(this.renderer.domElement);
    // =======================Loop======================
    this.animate();
    window.addEventListener("resize", () => billiards.onWindowResize(), false);
  }
}
class UiLayer {
  dom = document.createElement("canvas");
  ctx = this.dom.getContext("2d")!;
  _uiStatus;
  _billiards;
  constructor(status: Ref<UiStatus>, billiards: Billiards) {
    this._uiStatus = status;
    this._billiards = billiards;
    this.dom.setAttribute("id", "uiLayer");
  }
  mount(container: HTMLElement) {
    if (container instanceof HTMLElement) {
      container.appendChild(this.dom);
      const resize = () => {
        this.dom.width = this._billiards.renderer.domElement.clientWidth;
        this.dom.height = this._billiards.renderer.domElement.clientHeight;
      };
      resize();
      window.addEventListener("resize", resize);
    }
  }
  pointerMove = (e: PointerEvent) => {
    // const renderDom = this._billiards.renderer.domElement;
    const renderDom = this.dom;
    const whiteballUv = this._billiards.balls[0].position
      .clone()
      .project(this._billiards.camera);
    const whiteBallPosiScreen = {
      x: ((whiteballUv.x + 1) * renderDom.width) / 2,
      y: renderDom.height * (1 - (whiteballUv.y + 1) / 2),
    };
    if (this.ctx) {
      this.ctx.clearRect(0, 0, this.dom.width, this.dom.height);
      this.ctx.strokeStyle = "#fff";
      this.ctx.beginPath();
      this.ctx.moveTo(whiteBallPosiScreen.x, whiteBallPosiScreen.y);
      this.ctx.setLineDash([0]);
      this.ctx.lineTo(e.clientX, e.clientY);
      this.ctx.stroke();

      this.ctx.beginPath();
      this.ctx.moveTo(whiteBallPosiScreen.x, whiteBallPosiScreen.y);
      this.ctx.setLineDash([4]);
      this.ctx.lineTo(
        whiteBallPosiScreen.x * (1 + 10) - 10 * e.clientX,
        whiteBallPosiScreen.y * (1 + 10) - 10 * e.clientY
      );

      this.ctx.beginPath();
      this.ctx.moveTo(whiteBallPosiScreen.x, whiteBallPosiScreen.y);
      this.ctx.setLineDash([4]);
      this.ctx.lineTo(
        whiteBallPosiScreen.x * (1 + 10) - 10 * e.clientX,
        whiteBallPosiScreen.y * (1 + 10) - 10 * e.clientY
      );

      this.ctx.beginPath();
      this.ctx.moveTo(whiteBallPosiScreen.x, whiteBallPosiScreen.y);
      this.ctx.setLineDash([4]);
      this.ctx.lineTo(
        whiteBallPosiScreen.x * (1 + 10) - 10 * e.clientX,
        whiteBallPosiScreen.y * (1 + 10) - 10 * e.clientY
      );
      this.ctx.stroke();
    }
  };
  click = (e: MouseEvent) => {
    const whiteball = this._billiards.balls[0].position.clone();
    const mousePos = new THREE.Vector3(
      (e.clientX / this.dom.width) * 2 - 1,
      (-e.clientY / this.dom.height) * 2 + 1
    ).unproject(billiards.camera);
    mousePos.multiplyScalar(
      (this._billiards.camera.position.z - ball_radius) /
        (this._billiards.camera.position.z - mousePos.z)
    );
    let v = whiteball.sub(mousePos).multiplyScalar(1 / 10);
    this._billiards.balls[0].velocity.x = v.x;
    this._billiards.balls[0].velocity.y = v.y;
    this._uiStatus.value = UiStatus.wait;
  };
  enable() {
    this.ctx.clearRect(0, 0, this.dom.width, this.dom.height);
    this.dom.style.display = "block";
    this.dom.style.zIndex = "2";
    this.dom.addEventListener("pointermove", this.pointerMove);
    this.dom.addEventListener("click", this.click);
  }
  disable() {
    this.ctx.clearRect(0, 0, this.dom.width, this.dom.height);
    this.dom.style.display = "none";
    this.dom.style.zIndex = "-999";
    this.dom.removeEventListener("pointermove", this.pointerMove);
    this.dom.removeEventListener("click", this.click);
  }
}

export const billiards = new Billiards(uiStatus);
export const uiLayer = new UiLayer(uiStatus, billiards);

watchEffect(() => {
  switch (uiStatus.value) {
    case UiStatus.grab: {
      billiards.controls.activate();
      uiLayer.disable();
      break;
    }
    case UiStatus.shuttle: {
      billiards.controls.deactivate();
      // billiards.enableShuttle();
      uiLayer.enable();
      break;
    }
    case UiStatus.wait: {
      billiards.controls.deactivate();
      // billiards.disableShuttle();
      uiLayer.disable();
      break;
    }
  }
});
